Micron Document
๐ŸŽ–๏ธGitะฏั€ะฐ๐ŸŽ–๏ธ


Displaying Raw โ€ข Download

feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt bf929c20f274232e0eb8ddc19b2dc82403e99e5b (bf929c20) Text, 24.59 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)

Tff7b72package T7ee787org.meshtastic.feature.discovery

Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.Flow
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.flowOf
Tff7b72import T7ee787kotlinx.coroutines.test.TestScope
Tff7b72import T7ee787kotlinx.coroutines.test.UnconfinedTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787okio.ByteString
Tff7b72import T7ee787okio.ByteString.Companion.toByteString
Tff7b72import T7ee787org.meshtastic.core.common.di.ApplicationCoroutineScope
Tff7b72import T7ee787org.meshtastic.core.database.dao.DiscoveryDao
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoveredNodeEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoveryPresetResultEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoverySessionEntity
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.ChannelOption
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DataPacket
Tff7b72import T7ee787org.meshtastic.core.model.MyNodeInfo
Tff7b72import T7ee787org.meshtastic.core.model.Node
Tff7b72import T7ee787org.meshtastic.core.model.NodeAddress
Tff7b72import T7ee787org.meshtastic.core.repository.DiscoveryPacketCollector
Tff7b72import T7ee787org.meshtastic.core.repository.DiscoveryPacketCollectorRegistry
Tff7b72import T7ee787org.meshtastic.core.testing.FakeNodeRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioConfigRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioController
Tff7b72import T7ee787org.meshtastic.core.testing.FakeServiceRepository
Tff7b72import T7ee787org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider
Tff7b72import T7ee787org.meshtastic.proto.Config
Tff7b72import T7ee787org.meshtastic.proto.Data
Tff7b72import T7ee787org.meshtastic.proto.LocalConfig
Tff7b72import T7ee787org.meshtastic.proto.LocalStats
Tff7b72import T7ee787org.meshtastic.proto.MeshPacket
Tff7b72import T7ee787org.meshtastic.proto.PortNum
Tff7b72import T7ee787org.meshtastic.proto.Position
Tff7b72import T7ee787org.meshtastic.proto.Telemetry
Tff7b72import T7ee787org.meshtastic.proto.User
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e// region Inline fakes

T8b949e/** In-memory fake of [DiscoveryDao] for unit tests. */
Tff7b72private Tff7b72class T56d364FakeDiscoveryDao Tb4b4b4: Te6edf3DiscoveryDao Tb4b4b4{
Tff7b72private Tff7b72var Te6edf3nextSessionId Tff7b72= T79c0ff1L
Tff7b72private Tff7b72var Te6edf3nextPresetResultId Tff7b72= T79c0ff1L
Tff7b72private Tff7b72var Te6edf3nextNodeId Tff7b72= T79c0ff1L

Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableMapOfTff7b72<Tffa657LongTb4b4b4, Te6edf3DiscoverySessionEntityTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3presetResults Tff7b72= Te6edf3mutableMapOfTff7b72<Tffa657LongTb4b4b4, Te6edf3DiscoveryPresetResultEntityTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3discoveredNodes Tff7b72= Te6edf3mutableMapOfTff7b72<Tffa657LongTb4b4b4, Te6edf3DiscoveredNodeEntityTff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffinsertSessionTb4b4b4(Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72val Te6edf3id Tff7b72= Te6edf3nextSessionIdTff7b72+Tff7b72+
Te6edf3sessionsTff7b72[Te6edf3idTff7b72] Tff7b72= Te6edf3sessionTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3id Tff7b72= Te6edf3idTb4b4b4)
Tff7b72return Te6edf3id
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffupdateSessionTb4b4b4(Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4) Tb4b4b4{
Te6edf3sessionsTff7b72[Te6edf3sessionTb4b4b4.Te6edf3idTff7b72] Tff7b72= Te6edf3session
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffgetAllSessionsTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3ListTff7b72<Te6edf3DiscoverySessionEntityTff7b72>Tff7b72> Tff7b72=
Te6edf3flowOfTb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3sortedByDescending Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3timestamp Tb4b4b4}Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetSessionTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3DiscoverySessionEntity? Tff7b72= Te6edf3sessionsTff7b72[Te6edf3sessionIdTff7b72]

Tff7b72override Tff7b72fun Td2a8ffgetSessionFlowTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3DiscoverySessionEntity?Tff7b72> Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Te6edf3sessionsTff7b72[Te6edf3sessionIdTff7b72]Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffdeleteSessionTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4) Tb4b4b4{
Te6edf3sessionsTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3sessionIdTb4b4b4)
Tff7b72val Te6edf3resultIds Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sessionId Tff7b72=Tff7b72= Te6edf3sessionId Tb4b4b4}Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tb4b4b4}
Te6edf3resultIdsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3resultId Tff7b72-Tff7b72>
Te6edf3discoveredNodesTb4b4b4.Te6edf3entriesTb4b4b4.Te6edf3removeAll Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3presetResultId Tff7b72=Tff7b72= Te6edf3resultId Tb4b4b4}
Te6edf3presetResultsTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3resultIdTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffinsertPresetResultTb4b4b4(Te6edf3resultTb4b4b4: Te6edf3DiscoveryPresetResultEntityTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72val Te6edf3id Tff7b72= Te6edf3nextPresetResultIdTff7b72+Tff7b72+
Te6edf3presetResultsTff7b72[Te6edf3idTff7b72] Tff7b72= Te6edf3resultTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3id Tff7b72= Te6edf3idTb4b4b4)
Tff7b72return Te6edf3id
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffupdatePresetResultTb4b4b4(Te6edf3resultTb4b4b4: Te6edf3DiscoveryPresetResultEntityTb4b4b4) Tb4b4b4{
Te6edf3presetResultsTff7b72[Te6edf3resultTb4b4b4.Te6edf3idTff7b72] Tff7b72= Te6edf3result
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetPresetResultsTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3DiscoveryPresetResultEntityTff7b72> Tff7b72=
Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sessionId Tff7b72=Tff7b72= Te6edf3sessionId Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffgetPresetResultsFlowTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3ListTff7b72<Te6edf3DiscoveryPresetResultEntityTff7b72>Tff7b72> Tff7b72=
Te6edf3flowOfTb4b4b4(Te6edf3getPresetResultsSynchronousTb4b4b4(Te6edf3sessionIdTb4b4b4)Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffgetPresetResultsSynchronousTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3DiscoveryPresetResultEntityTff7b72> Tff7b72=
Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sessionId Tff7b72=Tff7b72= Te6edf3sessionId Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffinsertDiscoveredNodeTb4b4b4(Te6edf3nodeTb4b4b4: Te6edf3DiscoveredNodeEntityTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72val Te6edf3id Tff7b72= Te6edf3nextNodeIdTff7b72+Tff7b72+
Te6edf3discoveredNodesTff7b72[Te6edf3idTff7b72] Tff7b72= Te6edf3nodeTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3id Tff7b72= Te6edf3idTb4b4b4)
Tff7b72return Te6edf3id
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffinsertDiscoveredNodesTb4b4b4(Te6edf3nodesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DiscoveredNodeEntityTff7b72>Tb4b4b4) Tb4b4b4{
Te6edf3nodesTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3insertDiscoveredNodeTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffupdateDiscoveredNodeTb4b4b4(Te6edf3nodeTb4b4b4: Te6edf3DiscoveredNodeEntityTb4b4b4) Tb4b4b4{
Te6edf3discoveredNodesTff7b72[Te6edf3nodeTb4b4b4.Te6edf3idTff7b72] Tff7b72= Te6edf3node
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetDiscoveredNodesTb4b4b4(Te6edf3presetResultIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3DiscoveredNodeEntityTff7b72> Tff7b72=
Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3presetResultId Tff7b72=Tff7b72= Te6edf3presetResultId Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffgetDiscoveredNodesFlowTb4b4b4(Te6edf3presetResultIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3ListTff7b72<Te6edf3DiscoveredNodeEntityTff7b72>Tff7b72> Tff7b72=
Te6edf3flowOfTb4b4b4(Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3presetResultId Tff7b72=Tff7b72= Te6edf3presetResultId Tb4b4b4}Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetUniqueNodeNumsTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Tffa657LongTff7b72> Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3values
Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sessionId Tff7b72=Tff7b72= Te6edf3sessionId Tb4b4b4}
Tb4b4b4.Te6edf3flatMap Tb4b4b4{ Te6edf3pr Tff7b72-Tff7b72> Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3presetResultId Tff7b72=Tff7b72= Te6edf3prTb4b4b4.Te6edf3id Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3nodeNum Tb4b4b4}
Tb4b4b4.Te6edf3distinctTb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetUniqueNodeCountTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Tffa657Int Tff7b72= Te6edf3getUniqueNodeNumsTb4b4b4(Te6edf3sessionIdTb4b4b4)Tb4b4b4.Te6edf3size

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetMaxDistanceTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Tffa657Double? Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3values
Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sessionId Tff7b72=Tff7b72= Te6edf3sessionId Tb4b4b4}
Tb4b4b4.Te6edf3flatMap Tb4b4b4{ Te6edf3pr Tff7b72-Tff7b72> Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3presetResultId Tff7b72=Tff7b72= Te6edf3prTb4b4b4.Te6edf3id Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3distanceFromUser Tb4b4b4}
Tb4b4b4.Te6edf3maxOrNullTb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetSessionWithResultsTb4b4b4(Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3DiscoverySessionEntity? Tff7b72= Te6edf3sessionsTff7b72[Te6edf3sessionIdTff7b72]

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffmarkInterruptedSessionsTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3sessionsTb4b4b4.Te6edf3keysTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3key Tff7b72-Tff7b72>
Tff7b72val Te6edf3session Tff7b72= Te6edf3sessionsTff7b72[Te6edf3keyTff7b72]Tff7b72!!
Tff7b72if Tb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3completionStatus Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffin_progressTa5d6ff"Tb4b4b4) Tb4b4b4{
Te6edf3sessionsTff7b72[Te6edf3keyTff7b72] Tff7b72= Te6edf3sessionTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3completionStatus Tff7b72= Ta5d6ff"Ta5d6ffinterruptedTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Simple fake collector registry that tracks registration. */
Tff7b72private Tff7b72class T56d364FakeCollectorRegistry Tb4b4b4: Te6edf3DiscoveryPacketCollectorRegistry Tb4b4b4{
Tff7b72override Tff7b72var Te6edf3collectorTb4b4b4: Te6edf3DiscoveryPacketCollector? Tff7b72= Tff7b72null
Tb4b4b4}

T8b949e/** AI provider that is never available (no AI in tests). */
Tff7b72private Tff7b72class T56d364FakeAiProvider Tb4b4b4: Te6edf3DiscoverySummaryAiProvider Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3isAvailableTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgenerateSessionSummaryTb4b4b4(
Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4,
Te6edf3presetResultsTb4b4b4: Te6edf3ListTff7b72<Te6edf3DiscoveryPresetResultEntityTff7b72>Tb4b4b4,
Tb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Tff7b72null

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgeneratePresetSummaryTb4b4b4(Te6edf3resultTb4b4b4: Te6edf3DiscoveryPresetResultEntityTb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Tff7b72null
Tb4b4b4}

T8b949e// endregion

Tff7b72class T56d364DiscoveryScanEngineTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3radioController Tff7b72= Te6edf3FakeRadioControllerTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3serviceRepository Tff7b72= Te6edf3FakeServiceRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3nodeRepository Tff7b72= Te6edf3FakeNodeRepositoryTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3radioConfigRepository Tff7b72=
Te6edf3FakeRadioConfigRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3setLocalConfigDirectTb4b4b4(
Te6edf3LocalConfigTb4b4b4(
Te6edf3lora Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3LoRaConfigTb4b4b4(Te6edf3use_preset Tff7b72= Tff7b72trueTb4b4b4, Te6edf3modem_preset Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3collectorRegistry Tff7b72= Te6edf3FakeCollectorRegistryTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3discoveryDao Tff7b72= Te6edf3FakeDiscoveryDaoTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3aiProvider Tff7b72= Te6edf3FakeAiProviderTb4b4b4(Tb4b4b4)

T8b949e/** Creates a [DiscoveryScanEngine] wired to test dispatchers sharing the given [testScope]'s scheduler. */
Tff7b72private Tff7b72fun Td2a8ffcreateEngineTb4b4b4(Te6edf3testScopeTb4b4b4: Te6edf3TestScopeTb4b4b4)Tb4b4b4: Te6edf3DiscoveryScanEngine Tb4b4b4{
Tff7b72val Te6edf3testDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Te6edf3testScopeTb4b4b4.Te6edf3testSchedulerTb4b4b4)
Tff7b72val Te6edf3dispatchers Tff7b72= Te6edf3CoroutineDispatchersTb4b4b4(Te6edf3io Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3main Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3default Tff7b72= Te6edf3testDispatcherTb4b4b4)
Tff7b72val Te6edf3appScope Tff7b72=
Tff7b72object Tb4b4b4: T56d364ApplicationCoroutineScope Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3coroutineContext Tff7b72= Te6edf3testDispatcher Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3DiscoveryScanEngineTb4b4b4(
Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4,
Te6edf3serviceRepository Tff7b72= Te6edf3serviceRepositoryTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3radioConfigRepository Tff7b72= Te6edf3radioConfigRepositoryTb4b4b4,
Te6edf3collectorRegistry Tff7b72= Te6edf3collectorRegistryTb4b4b4,
Te6edf3discoveryDao Tff7b72= Te6edf3discoveryDaoTb4b4b4,
Te6edf3aiProvider Tff7b72= Te6edf3aiProviderTb4b4b4,
Te6edf3applicationScope Tff7b72= Te6edf3appScopeTb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3dispatchersTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72val Te6edf3testPresets Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4)

T8b949e/**
* After [DiscoveryScanEngine.startScan], the state is set to [DiscoveryScanState.Shifting] synchronously. This
* helper asserts that the engine is active โ€” no real-time wait needed.
*/
Tff7b72private Tff7b72fun Td2a8ffassertScanActiveTb4b4b4(Te6edf3engineTb4b4b4: Te6edf3DiscoveryScanEngineTb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4, Ta5d6ff"Ta5d6ffEngine should be active after startScanTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Waits briefly for the scan loop (running on test dispatcher) to complete its per-preset initialization
* (collection clearing). Call before sending packets to avoid a race where the scan loop's `collectedNodes.clear()`
* wipes out test-injected data.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffawaitScanLoopInitTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}

T8b949e// region Helper factories

Tff7b72private Tff7b72fun Td2a8ffcreateMyNodeInfoTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657Int Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0Tb4b4b4) Tff7b72= Te6edf3MyNodeInfoTb4b4b4(
Te6edf3myNodeNum Tff7b72= Te6edf3nodeNumTb4b4b4,
Te6edf3hasGPS Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3model Tff7b72= Ta5d6ff"Ta5d6ffTestModelTa5d6ff"Tb4b4b4,
Te6edf3firmwareVersion Tff7b72= Ta5d6ff"Ta5d6ff2.0.0Ta5d6ff"Tb4b4b4,
Te6edf3couldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3shouldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3currentPacketId Tff7b72= T79c0ff1LTb4b4b4,
Te6edf3messageTimeoutMsec Tff7b72= T79c0ff5T79c0ff0T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3minAppVersion Tff7b72= T79c0ff1Tb4b4b4,
Te6edf3maxChannels Tff7b72= T79c0ff8Tb4b4b4,
Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3channelUtilization Tff7b72= T79c0ff0fTb4b4b4,
Te6edf3airUtilTx Tff7b72= T79c0ff0fTb4b4b4,
Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6fftest-deviceTa5d6ff"Tb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffcreateNodeWithPositionTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4, Te6edf3latITb4b4b4: Tffa657Int Tff7b72= T79c0ff0Tb4b4b4, Te6edf3lonITb4b4b4: Tffa657Int Tff7b72= T79c0ff0Tb4b4b4) Tff7b72= Te6edf3NodeTb4b4b4(
Te6edf3num Tff7b72= Te6edf3numTb4b4b4,
Te6edf3user Tff7b72= Te6edf3UserTb4b4b4(Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ff!Tffd700${Te6edf3numTb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4, Te6edf3short_name Tff7b72= Ta5d6ff"Ta5d6ffTTffd700$Te6edf3numTa5d6ff"Tb4b4b4, Te6edf3long_name Tff7b72= Ta5d6ff"Ta5d6ffTest Node Tffd700$Te6edf3numTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3position Tff7b72= Te6edf3PositionTb4b4b4(Te6edf3latitude_i Tff7b72= Te6edf3latITb4b4b4, Te6edf3longitude_i Tff7b72= Te6edf3lonITb4b4b4)Tb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffcreatePositionMeshPacketTb4b4b4(
Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3latITb4b4b4: Tffa657IntTb4b4b4,
Te6edf3lonITb4b4b4: Tffa657IntTb4b4b4,
Te6edf3snrTb4b4b4: Tffa657Float Tff7b72= T79c0ff5.5fTb4b4b4,
Te6edf3rssiTb4b4b4: Tffa657Int Tff7b72= Tff7b72-T79c0ff7T79c0ff0Tb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3MeshPacket Tb4b4b4{
Tff7b72val Te6edf3posPayload Tff7b72= Te6edf3PositionTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3encodeTb4b4b4(Te6edf3PositionTb4b4b4(Te6edf3latitude_i Tff7b72= Te6edf3latITb4b4b4, Te6edf3longitude_i Tff7b72= Te6edf3lonITb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3data Tff7b72= Te6edf3DataTb4b4b4(Te6edf3portnum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3POSITION_APPTb4b4b4, Te6edf3payload Tff7b72= Te6edf3posPayloadTb4b4b4)
Tff7b72return Te6edf3MeshPacketTb4b4b4(Te6edf3from Tff7b72= Te6edf3fromTb4b4b4, Te6edf3decoded Tff7b72= Te6edf3dataTb4b4b4, Te6edf3rx_snr Tff7b72= Te6edf3snrTb4b4b4, Te6edf3rx_rssi Tff7b72= Te6edf3rssiTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffcreateTelemetryWithLocalStatsTb4b4b4(Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4, Te6edf3localStatsTb4b4b4: Te6edf3LocalStatsTb4b4b4)Tb4b4b4: Te6edf3MeshPacket Tb4b4b4{
Tff7b72val Te6edf3telPayload Tff7b72= Te6edf3TelemetryTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3encodeTb4b4b4(Te6edf3TelemetryTb4b4b4(Te6edf3local_stats Tff7b72= Te6edf3localStatsTb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3data Tff7b72= Te6edf3DataTb4b4b4(Te6edf3portnum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3TELEMETRY_APPTb4b4b4, Te6edf3payload Tff7b72= Te6edf3telPayloadTb4b4b4)
Tff7b72return Te6edf3MeshPacketTb4b4b4(Te6edf3from Tff7b72= Te6edf3fromTb4b4b4, Te6edf3decoded Tff7b72= Te6edf3dataTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffcreateDataPacketTb4b4b4(Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3DataPacket Tff7b72= Te6edf3DataPacketTb4b4b4(
Te6edf3to Tff7b72= Te6edf3NodeAddressTb4b4b4.Te6edf3ID_BROADCASTTb4b4b4,
Te6edf3bytes Tff7b72= Te6edf3ByteStringTb4b4b4.Te6edf3EMPTYTb4b4b4,
Te6edf3dataType Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3POSITION_APPTb4b4b4.Te6edf3valueTb4b4b4,
Te6edf3from Tff7b72= Ta5d6ff"Ta5d6ff!Tffd700${Te6edf3fromTb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4,
Te6edf3hopStart Tff7b72= T79c0ff3Tb4b4b4,
Te6edf3hopLimit Tff7b72= T79c0ff3Tb4b4b4,
Tb4b4b4)

T8b949e// endregion

Tf0883e@Test
Tff7b72fun Td2a8ffstartScanCreatesSessionAndRegistersCollectorTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)

T8b949e// Session should be persisted (happens synchronously inside startScan)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffin_progressTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3presetsScannedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3homePresetTb4b4b4)

T8b949e// Collector should be registered (synchronous inside startScan)
Te6edf3assertNotNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collector Tff7b72=Tff7b72=Tff7b72= Te6edf3engineTb4b4b4)

T8b949e// currentSession should be populated
Tff7b72val Te6edf3currentSession Tff7b72= Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3value
Te6edf3assertNotNullTb4b4b4(Te6edf3currentSessionTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3idTb4b4b4, Te6edf3currentSessionTb4b4b4.Te6edf3idTb4b4b4)

T8b949e// Wait for scan loop to start then clean up
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffstopScanPersistsResultsAndTransitionsToIdleTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

T8b949e// Verify scan is active
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)

T8b949e// State should be Complete(Cancelled)
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)
Tff7b72val Te6edf3completeState Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3Complete
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3CancelledTb4b4b4, Te6edf3completeStateTb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)

T8b949e// Collector should be unregistered
Te6edf3assertNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4)

T8b949e// Session should be finalized with "stopped" status
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffstoppedTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3completionStatusTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffcompleteScanCreatesSessionWithInProgressStatusTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff5Tb4b4b4)

T8b949e// Immediately after startScan, the session should exist with "in_progress"
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffin_progressTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3completionStatusTb4b4b4)

T8b949e// Wait for the scan loop to start, then verify active
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffemptyPresetDwellPersistsZeroResultEntryTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

T8b949e// Stop without receiving any packets โ€” forces persistCurrentDwellResults
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)

T8b949e// Should have a preset result with zero unique nodes
Tff7b72val Te6edf3presetResults Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3presetResultsTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffExpected at least one preset resultTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3presetNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3uniqueNodesTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3messageCountTb4b4b4)

T8b949e// No discovered nodes
Te6edf3assertTrueTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffpacketCollectionPopulatesNodeDataTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3myNodeNum Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNumTb4b4b4)Tb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setNodesTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3createNodeWithPositionTb4b4b4(Te6edf3num Tff7b72= Te6edf3myNodeNumTb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff0T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff0T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

T8b949e// Wait for Dwell state
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}

T8b949e// Simulate receiving a position packet
Tff7b72val Te6edf3meshPacket Tff7b72=
Te6edf3createPositionMeshPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff3T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3snr Tff7b72= T79c0ff5.5fTb4b4b4, Te6edf3rssi Tff7b72= Tff7b72-T79c0ff7T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3dataPacketTb4b4b4)

T8b949e// Stop scan to persist results
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)

T8b949e// Should have one discovered node with lat/lon
Tff7b72val Te6edf3nodes Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3nodesTb4b4b4.Te6edf3sizeTb4b4b4)

Tff7b72val Te6edf3node Tff7b72= Te6edf3nodesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5LTb4b4b4, Te6edf3nodeTb4b4b4.Te6edf3nodeNumTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3latitudeTb4b4b4, Ta5d6ff"Ta5d6ffNode should have latitudeTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3longitudeTb4b4b4, Ta5d6ff"Ta5d6ffNode should have longitudeTa5d6ff"Tb4b4b4)
T8b949e// latitude_i = 377749300 โ†’ 37.77493
Te6edf3assertTrueTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3latitudeTff7b72!! Tff7b72> T79c0ff3T79c0ff7.7 Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3latitudeTff7b72!! Tff7b72< T79c0ff3T79c0ff7.8Tb4b4b4, Ta5d6ff"Ta5d6ffLatitude should be ~37.77Ta5d6ff"Tb4b4b4)
T8b949e// longitude_i = -1224194200 โ†’ -122.41942
Te6edf3assertTrueTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3longitudeTff7b72!! Tff7b72< Tff7b72-T79c0ff1T79c0ff2T79c0ff2.4 Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3longitudeTff7b72!! Tff7b72> Tff7b72-T79c0ff1T79c0ff2T79c0ff2.5Tb4b4b4, Ta5d6ff"Ta5d6ffLongitude should be ~-122.42Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff5.5fTb4b4b4, Te6edf3nodeTb4b4b4.Te6edf3snrTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72-T79c0ff7T79c0ff0Tb4b4b4, Te6edf3nodeTb4b4b4.Te6edf3rssiTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftelemetryWithLocalStatsPopulatesRfHealthTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

T8b949e// Wait for Dwell state and ensure sessionId is set
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3Dwell Tff7b72|Tff7b72| Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}

T8b949e// Send a telemetry packet with local_stats
Tff7b72val Te6edf3localStats Tff7b72=
Te6edf3LocalStatsTb4b4b4(
Te6edf3num_packets_tx Tff7b72= T79c0ff1T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3num_packets_rx Tff7b72= T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3num_packets_rx_bad Tff7b72= T79c0ff5Tb4b4b4,
Te6edf3num_rx_dupe Tff7b72= T79c0ff1T79c0ff0Tb4b4b4,
Te6edf3num_tx_relay Tff7b72= T79c0ff1T79c0ff5Tb4b4b4,
Te6edf3num_tx_relay_canceled Tff7b72= T79c0ff2Tb4b4b4,
Te6edf3num_online_nodes Tff7b72= T79c0ff3Tb4b4b4,
Te6edf3num_total_nodes Tff7b72= T79c0ff1T79c0ff0Tb4b4b4,
Te6edf3uptime_seconds Tff7b72= T79c0ff3T79c0ff6T79c0ff0T79c0ff0Tb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3meshPacket Tff7b72= Te6edf3createTelemetryWithLocalStatsTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4, Te6edf3localStats Tff7b72= Te6edf3localStatsTb4b4b4)
Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3dataPacketTb4b4b4)

T8b949e// Stop to persist
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)

T8b949e// The preset result should have RF health fields from local_stats
Tff7b72val Te6edf3presetResults Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3presetResultsTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffExpected a preset resultTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numPacketsTxTb4b4b4, Ta5d6ff"Ta5d6ffnumPacketsTx should be 100Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numPacketsRxTb4b4b4, Ta5d6ff"Ta5d6ffnumPacketsRx should be 200Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff5Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numPacketsRxBadTb4b4b4, Ta5d6ff"Ta5d6ffnumPacketsRxBad should be 5Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numRxDupeTb4b4b4, Ta5d6ff"Ta5d6ffnumRxDupe should be 10Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff5Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numTxRelayTb4b4b4, Ta5d6ff"Ta5d6ffnumTxRelay should be 15Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numTxRelayCanceledTb4b4b4, Ta5d6ff"Ta5d6ffnumTxRelayCanceled should be 2Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numOnlineNodesTb4b4b4, Ta5d6ff"Ta5d6ffnumOnlineNodes should be 3Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numTotalNodesTb4b4b4, Ta5d6ff"Ta5d6ffnumTotalNodes should be 10Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3T79c0ff6T79c0ff0T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3uptimeSecondsTb4b4b4, Ta5d6ff"Ta5d6ffuptimeSeconds should be 3600Ta5d6ff"Tb4b4b4)

T8b949e// Packet success/failure rates should be computed
T8b949e// success = (200 - 5) / 200 * 100 = 97.5
T8b949e// failure = 5 / 200 * 100 = 2.5
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3packetSuccessRate Tff7b72> T79c0ff9T79c0ff7.0Tb4b4b4, Ta5d6ff"Ta5d6ffSuccess rate should be ~97.5%Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3packetFailureRate Tff7b72> T79c0ff2.0Tb4b4b4, Ta5d6ff"Ta5d6ffFailure rate should be ~2.5%Ta5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffuserPositionCapturedAtScanStartTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3myNodeNum Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNumTb4b4b4)Tb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setNodesTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3createNodeWithPositionTb4b4b4(Te6edf3num Tff7b72= Te6edf3myNodeNumTb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff3T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)

Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
T8b949e// User position should be captured from the own node
T8b949e// latitude_i = 377749300 โ†’ 37.77493
Te6edf3assertTrueTb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3userLatitude Tff7b72> T79c0ff3T79c0ff7.7 Tff7b72&Tff7b72& Te6edf3sessionTb4b4b4.Te6edf3userLatitude Tff7b72< T79c0ff3T79c0ff7.8Tb4b4b4, Ta5d6ff"Ta5d6ffUser lat should be ~37.77Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3userLongitude Tff7b72< Tff7b72-T79c0ff1T79c0ff2T79c0ff2.4 Tff7b72&Tff7b72& Te6edf3sessionTb4b4b4.Te6edf3userLongitude Tff7b72> Tff7b72-T79c0ff1T79c0ff2T79c0ff2.5Tb4b4b4, Ta5d6ff"Ta5d6ffUser lon should be ~-122.42Ta5d6ff"Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffdistanceFromUserCalculatedForDiscoveredNodesTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3myNodeNum Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNumTb4b4b4)Tb4b4b4)
T8b949e// User at San Francisco (37.7749, -122.4194)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setNodesTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3createNodeWithPositionTb4b4b4(Te6edf3num Tff7b72= Te6edf3myNodeNumTb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff0T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff0T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

T8b949e// Wait for Dwell state
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}

T8b949e// Discovered node at Oakland (37.8044, -122.2712) โ€” roughly 15 km away
Tff7b72val Te6edf3meshPacket Tff7b72= Te6edf3createPositionMeshPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff5T79c0ff4T79c0ff3T79c0ff2T79c0ff1Tb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff8T79c0ff0T79c0ff4T79c0ff4T79c0ff0T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff2T79c0ff7T79c0ff1T79c0ff2T79c0ff0T79c0ff0T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff5T79c0ff4T79c0ff3T79c0ff2T79c0ff1Tb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3dataPacketTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3nodes Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3nodesTb4b4b4.Te6edf3sizeTb4b4b4)

Tff7b72val Te6edf3node Tff7b72= Te6edf3nodesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTb4b4b4, Ta5d6ff"Ta5d6ffDistance from user should be computedTa5d6ff"Tb4b4b4)
T8b949e// SF to Oakland is roughly 13โ€“17 km
Te6edf3assertTrueTb4b4b4(
Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTff7b72!! Tff7b72> T79c0ff1T79c0ff0Te6edf3_000 Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTff7b72!! Tff7b72< T79c0ff2T79c0ff5Te6edf3_000Tb4b4b4,
Ta5d6ff"Ta5d6ffDistance should be between 10km and 25km, was Tffd700${Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTffd700}Ta5d6ffmTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e// region Home-preset restoration (the one config-mutating, safety-critical behavior of a scan)

Tf0883e@Test
Tff7b72fun Td2a8ffreconnectTimeoutAbortsRestoresHomePresetAndFinalizesSessionTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
T8b949e// Scan a preset different from the seeded home (LONG_FAST) so a restore-to-home is observable.
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

T8b949e// The radio fails to come back after the preset shift (firmware reboots on a LoRa config change).
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// The abort path must reach a terminal Failed state, finalize the session, and restore the home preset โ€”
T8b949e// none of which happened before the fix, because cancelScanInternal() cancelled this coroutine first.
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3FailedTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4, Ta5d6ff"Ta5d6ffcollector should be unregisteredTa5d6ff"Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6fffailedTa5d6ff"Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
T8b949e// The last LoRa config applied is the home preset (LONG_FAST), not the scan preset (SHORT_FAST).
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffstopScanRestoresHomePresetTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)

Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4) T8b949e// let the applicationScope restore complete

Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffnormalCompletionRestoresHomePresetTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4) T8b949e// connection stays Connected, so the scan runs to completion

Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3SuccessTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffcompleteTa5d6ff"Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}

T8b949e// endregion
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.15s